package com.iflytek.jzcpx.procuracy.web.config;

import javax.annotation.PreDestroy;
import java.lang.management.ManagementFactory;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

import cn.hutool.core.date.LocalDateTimeUtil;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.iflytek.jzcpx.procuracy.web.sharding.GuidGenerator;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.reflection.MetaObject;
import org.mybatis.spring.annotation.MapperScan;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;

/**
 * 数据库相关配置
 *
 * @author <a href=mailto:ktyi@iflytek.com>伊开堂</a>
 * @date 2019/4/15 11:06
 */
@Configuration
@MapperScan("com.iflytek.zhbg.jzpdt.dao")
public class MybatisPlusConfig {
    protected final Logger logger = LoggerFactory.getLogger(MybatisPlusConfig.class);

    private RLock lock = null;
    private String lock_name;
    private DbType dbType = DbType.MYSQL;
    private String prefix = null;

    @Autowired
    public void setEnvironment(Environment environment) {
        String driverClassName = environment.getProperty("spring.datasource.driver-class-name");
        // 通过 JDBC驱动确定是否为达梦数据库
        if (StringUtils.isNotBlank(driverClassName)) {
            switch (driverClassName) {
                case "dm.jdbc.driver.DmDriver":
                    dbType = DbType.DM;
                    break;
                case "oracle.jdbc.driver.OracleDriver":
                    dbType = DbType.ORACLE;
                    break;
                default:
                    System.out.println("use mysql as db type by default");
            }
        }

        prefix = environment.getProperty("mybatis-plus.global-config.db-config.table-prefix");
    }

    /**
     * 分页插件
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        PaginationInnerInterceptor innerInterceptor = new PaginationInnerInterceptor(dbType);
        innerInterceptor.setMaxLimit(1000L);
        interceptor.addInnerInterceptor(innerInterceptor);
        return interceptor;
    }

    /*
    *@TableField(value = "create_time", fill = FieldFill.INSERT)
    private Date createTime;

    @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
    private Date updateTime;
    */
    @Bean
    @ConditionalOnMissingBean
    public MetaObjectHandler metaObjectHandler() {
        // 插入时自动注入createTime/updateTime, 更新时自动注入updateTime
        return new MetaObjectHandler() {
            private static final String CREATE_TIME_FIELD = "createTime";
            private static final String UPDATE_TIME_FIELD = "updateTime";

            @Override
            public void insertFill(MetaObject metaObject) {
                Class<?> createTimeType = metaObject.getSetterType(CREATE_TIME_FIELD);
                Class<?> updateTimeType = metaObject.getSetterType(UPDATE_TIME_FIELD);
                Object createTime = this.getFieldValByName(CREATE_TIME_FIELD, metaObject);
                Object updateTime = this.getFieldValByName(UPDATE_TIME_FIELD, metaObject);

                if (LocalDateTime.class.equals(createTimeType)) {
                    LocalDateTime now = LocalDateTime.now();
                    if (createTime == null) {
                        this.strictInsertFill(metaObject, CREATE_TIME_FIELD, LocalDateTime.class, now);
                    }

                    filleUpdateTime(metaObject, updateTimeType, updateTime, LocalDateTimeUtil.toEpochMilli(now));
                }
                else if (Instant.class.equals(createTimeType)) {
                    Instant now = Instant.now();
                    if (createTime == null) {
                        this.strictInsertFill(metaObject, CREATE_TIME_FIELD, Instant.class, now);
                    }

                    filleUpdateTime(metaObject, updateTimeType, updateTime, LocalDateTimeUtil.toEpochMilli(now));
                }
                else if (Date.class.equals(createTimeType)) {
                    Date now = new Date();
                    if (createTime == null) {
                        this.strictInsertFill(metaObject, CREATE_TIME_FIELD, Date.class, now);
                    }

                    filleUpdateTime(metaObject, updateTimeType, updateTime, now.getTime());
                }
            }

            private void filleUpdateTime(MetaObject metaObject, Class<?> fieldType, Object fieldValue, long timestamp) {
                if (fieldValue == null) {
                    if (LocalDateTime.class.equals(fieldType)) {
                        this.strictInsertFill(metaObject, UPDATE_TIME_FIELD, LocalDateTime.class,
                                              LocalDateTimeUtil.of(timestamp));
                    }
                    else if (Instant.class.equals(fieldType)) {
                        this.strictInsertFill(metaObject, UPDATE_TIME_FIELD, Instant.class,
                                              Instant.ofEpochMilli(timestamp));
                    }
                    else if (Date.class.equals(fieldType)) {
                        this.strictInsertFill(metaObject, UPDATE_TIME_FIELD, Date.class, new Date(timestamp));
                    }
                }
            }

            @Override
            public void updateFill(MetaObject metaObject) {
                Class<?> fieldType = metaObject.getSetterType(UPDATE_TIME_FIELD);
                Object updateTime = this.getFieldValByName(UPDATE_TIME_FIELD, metaObject);
                if (updateTime == null) {
                    if (LocalDateTime.class.equals(fieldType)) {
                        this.strictUpdateFill(metaObject, UPDATE_TIME_FIELD, LocalDateTime.class, LocalDateTime.now());
                    }
                    else if (Instant.class.equals(fieldType)) {
                        this.strictUpdateFill(metaObject, UPDATE_TIME_FIELD, Instant.class, Instant.now());
                    }
                    else if (Date.class.equals(fieldType)) {
                        this.strictUpdateFill(metaObject, UPDATE_TIME_FIELD, Date.class, new Date());
                    }
                }
            }
        };
    }

    /*
    实体的主键生成策略必须使用INPUT
    @KeySequence(value = "SEQ_ORACLE_STRING_KEY", clazz = String.class)
    public class YourEntity {

        @TableId(value = "ID_STR", type = IdType.INPUT)
        private String idStr;

    }
    */

    /*
    主键生成策略为ASSIGN_ID, 适用类型: Long,Integer,String
     * */
    @Bean
    public IdentifierGenerator idGenerator(GuidGenerator guidGenerator) {
        return new IdentifierGenerator() {
            @Override
            public Number nextId(Object entity) {
                return guidGenerator.nextId();
            }
        };
    }

    @Bean
    public GuidGenerator guidGenerator(RedissonClient redissonClient) {
        long workerId = requestWorkerId(redissonClient, GuidGenerator.maxWorkerId);
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            destroy();
        }));
        return new GuidGenerator(0L, workerId);
    }

    @PreDestroy
    public void destroy() {
        if (lock != null && lock.isHeldByCurrentThread()) {
            logger.info("销毁ID生成器分布式锁: " + lock_name);
            lock.unlock();
            lock = null;
        }
    }

    private long requestWorkerId(RedissonClient redissonClient, long maxWorkerId) {
        for (long i = 0; i < maxWorkerId; i++) {
            lock_name = "SHARDING_WORKER_" + i;
            RLock lock = redissonClient.getLock(lock_name);
            if (lock.tryLock()) {
                logger.info("获取到ID生成器分布式锁: " + lock_name);
                return i;
            }
        }
        throw new RuntimeException("无法生成ID生成器, 请检查Redis");
    }

    // 默认的雪花算法生成的long可能造成前端溢出, 因此生成最多使用53bits的long
    private static class IdGenerator {
        private static final Logger logger = LoggerFactory.getLogger(IdGenerator.class);

        private static final IdGenerator.Sequence SEQUENCE = new IdGenerator.Sequence();

        private IdGenerator() {
        }

        public static long nextId() {
            return SEQUENCE.nextId();
        }

        public static String decode(long id) {
            return SEQUENCE.decode(id);
        }

        public static void main(String[] args) {
            System.out.println(nextId());
        }

        private static class Sequence {
            /*
             * <br>
             * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 <br>
             * <br>
             * 1位符号位 + 41位时间戳(69年) + 5位节点标识(32) + 5位秒内序列(8/ms)
             */

            /**
             * 时间起始标记点 2022-01-01 00:00:00.000，作为基准，一旦确定不能变动
             */
            private final long twepoch = 1640966400000L;
            /**
             * 进程标识位数
             */
            private final long workerIdBits = 5L;
            /**
             * 秒内自增位
             */
            private final long sequenceBits = 5L;
            private final long workerIdShift = sequenceBits;
            /**
             * 时间戳左移动位
             */
            private final long timestampLeftShift = sequenceBits + workerIdBits;
            private final long sequenceMask = -1L ^ (-1L << sequenceBits);

            /**
             * 进程标识 ID
             */
            private final long workerId;

            /**
             * 毫秒内序列
             */
            private long sequence = 0L;

            /**
             * 上次生产 ID 时间戳
             */
            private long lastTimestamp = -1L;

            public Sequence() {
                long datacenterId = getDatacenterId(0);
                this.workerId = getWorkerId(datacenterId, 32);
            }

            /**
             * 获取 maxWorkerId
             */
            protected static long getWorkerId(long datacenterId, long maxWorkerId) {
                StringBuilder mpid = new StringBuilder();
                mpid.append(datacenterId);
                String name = ManagementFactory.getRuntimeMXBean().getName();
                if (StringUtils.isNotEmpty(name)) {
                    /*
                     * GET jvmPid
                     */
                    mpid.append(name.split("@")[0]);
                }
                /*
                 * MAC + PID 的 hashcode 获取16个低位
                 */
                return (mpid.toString().hashCode() & 0xffff) % (maxWorkerId + 1);
            }

            /**
             * 数据标识id部分
             */
            protected static long getDatacenterId(long maxDatacenterId) {
                long id = 0L;
                try {
                    InetAddress ip = InetAddress.getLocalHost();
                    NetworkInterface network = NetworkInterface.getByInetAddress(ip);
                    if (network == null) {
                        id = 1L;
                    }
                    else {
                        byte[] mac = network.getHardwareAddress();
                        if (null != mac) {
                            id = ((0x000000FF & (long) mac[mac.length - 1]) |
                                    (0x0000FF00 & (((long) mac[mac.length - 2]) << 8))) >> 6;
                            id = id % (maxDatacenterId + 1);
                        }
                    }
                }
                catch (Exception e) {
                    logger.warn(" getDatacenterId: " + e.getMessage());
                }
                return id;
            }

            /**
             * 获取下一个 ID
             *
             * @return 下一个 ID
             */
            public synchronized long nextId() {
                long timestamp = timeGen();
                //闰秒
                if (timestamp < lastTimestamp) {
                    long offset = lastTimestamp - timestamp;
                    if (offset <= 5) {
                        try {
                            wait(offset << 1);
                            timestamp = timeGen();
                            if (timestamp < lastTimestamp) {
                                throw new RuntimeException(String.format(
                                        "Clock moved backwards.  Refusing to generate id for %d milliseconds", offset));
                            }
                        }
                        catch (Exception e) {
                            throw new RuntimeException(e);
                        }
                    }
                    else {
                        throw new RuntimeException(
                                String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds",
                                              offset));
                    }
                }

                if (lastTimestamp == timestamp) {
                    // 相同毫秒内，序列号自增
                    sequence = (sequence + 1) & sequenceMask;
                    if (sequence == 0) {
                        // 同一毫秒的序列数已经达到最大
                        timestamp = tilNextMillis(lastTimestamp);
                    }
                }
                else {
                    // 不同毫秒内，序列号置为 1 - 100 随机数
                    sequence = ThreadLocalRandom.current().nextLong(1, 100);
                }

                lastTimestamp = timestamp;

                // 时间戳部分 | 数据中心部分 | 机器标识部分 | 序列号部分
                return ((timestamp - twepoch) << timestampLeftShift) | (workerId << workerIdShift) | sequence;
            }

            protected long tilNextMillis(long lastTimestamp) {
                long timestamp = timeGen();
                while (timestamp <= lastTimestamp) {
                    timestamp = timeGen();
                }
                return timestamp;
            }

            protected long timeGen() {
                return IdGenerator.SystemClock.now();
            }

            public String decode(long id) {
                long date = (id >> timestampLeftShift) + twepoch;
                String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date(date));

                return time;
            }
        }

        private static class SystemClock {

            private final long period;
            private final AtomicLong now;

            public static final IdGenerator.SystemClock INSTANCE = new IdGenerator.SystemClock(1);

            private SystemClock(long period) {
                this.period = period;
                this.now = new AtomicLong(System.currentTimeMillis());
                scheduleClockUpdating();
            }

            public static long now() {
                return INSTANCE.currentTimeMillis();
            }

            private void scheduleClockUpdating() {
                ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(runnable -> {
                    Thread thread = new Thread(runnable, "System Clock");
                    thread.setDaemon(true);
                    return thread;
                });
                scheduler.scheduleAtFixedRate(() -> now.set(System.currentTimeMillis()), period, period, TimeUnit.MILLISECONDS);
            }

            private long currentTimeMillis() {
                return now.get();
            }
        }
    }
}
